/*\ chat parrot v0.4.2
|*|
|*| TODO: get list of chatters
\*/


integer DEBUG = FALSE ;

string  SERVER_URL        = "https://CHANGEME" ;
integer SHOULD_VERIFY_SSL = TRUE ;

string  GRID_NAME        = "CHANGEME" ;
integer HAS_CUSTOM_NAMES = TRUE ; // does server allow arbitrary choice of last name
string  CLIENTS_PATH     = "/clients.json" ;
string  PING_PATH        = "/clients/{{CLIENT_NAME}}/ping.json" ;
string  ROOMS_PATH       = "/clients/{{CLIENT_NAME}}/rooms.json" ;
string  BRIDGE_PATH      = "/clients/{{CLIENT_NAME}}/bridge.json" ;
string  MESSAGE_PATH     = "/clients/{{CLIENT_NAME}}/message.json" ;
string  MESSAGES_PATH    = "/clients/{{CLIENT_NAME}}/messages.json" ;
string  OWNER_NAME ;             // Init()
string  REGION_NAME ;            // Init()
string  PARCEL_NAME ;            // Init()
string  CLIENT_NAME ;            // Init()
string  LOGIN_URL   ;            // Init()
string  PING_URL ;               // Init()
string  ROOMS_URL ;              // Init()
string  BRIDGE_URL ;             // Init()
string  MESSAGE_URL ;            // Init()
string  MESSAGES_URL ;           // Init()
string  CLIENT_SECRET ;          // ParseLoginResponse()
string  ROOM_SECRET ;            // ParseLoginResponse()
string  NOTECARD_CLIENT_SECRET ; // ParseLoginResponse()

// user messages and dialog prompts
integer DLG_CH                = -99 ;
string  OFF_DLG_BTN           = "Off" ;
string  ON_DLG_BTN            = "On" ;
string  BRIDGE_DLG_BTN        = "Join Room" ;
string  INIT_MSG              = "Connecting" ;
string  RESETTING_MSG         = "Re-connecting" ;
string  READY_MSG             = "Ready" ;
string  RUNNING_MSG           = "Listening" ;
string  IDLE_MSG              = "Not listening" ;
string  SERVER_ERROR_MSG      = "Invalid server response" ;
string  INVALID_SECRET_MSG    = "Client secret invalid - Try again later" ;
string  ROOMS_MSG             = "The following public chat rooms are available to join:" ;
string  BRIDGE_PROMPT         = "Type your choice in local chat (e.g. #42):" ;
string  NO_ROOMS_MSG          = "There are no other rooms to join" ;
string  BRIDGE_SELECT_TIMEOUT = "Cancelling room join" ;
string  BRIDGE_FAIL_MSG       = "Could not join room" ;
string  BRIDGE_TIMEOUT_MSG    = "Timeout waiting to join room" ;
string  JOINED_ROOM_MSG       = "Joined room with: " ;
string  DLG_RUNNING_PROMPT    = "Polly wanna shuddup?" ;
string  DLG_IDLE_PROMPT       = "Polly wanna cracker?" ;
vector  TEXT_COLOR            = <1.0 , 1.0 , 0.0> ;

// JSON string parsing
string ID_SENTINAL       = "{{CLIENT_NAME}}" ;
string DUPLICATE_SECRET  = "exists" ;
string SECRET_TOKEN      = "\"secret\":\"" ;      integer SECRET_TOKEN_LEN      = 10 ;
string ROOM_SECRET_TOKEN = "\"room_secret\":\"" ; integer ROOM_SECRET_TOKEN_LEN = 15 ;
string CLIENTS_TOKEN     = "\"clients\":\"" ;     integer CLIENTS_TOKEN_LEN     = 11 ;
string ID_TOKEN          = "\"id\":" ;            integer ID_TOKEN_LEN          = 5 ;
string NICK_TOKEN        = "\"nick\":\"" ;        integer NICK_TOKEN_LEN        = 8 ;
string TEXT_TOKEN        = "\"text\":\"" ;        integer TEXT_TOKEN_LEN        = 8 ;
string INT_SEP_TOKEN     = ",\"" ;
string STRING_SEP_TOKEN  = "\",\"" ;
string STRING_END_TOKEN  = "\"}" ;

// http
integer HTTP_STATUS_SUCCESS      = 200 ;
integer HTTP_STATUS_UNAUTHORIZED = 401 ;
list    JSON_POST_PARAMS         = [ HTTP_METHOD      , "POST"             ,
                                     HTTP_MIMETYPE    , "application/json" ,
                                     HTTP_VERIFY_CERT , FALSE              ] ;

// timers
float TIMEOUT     = 1500.0 ;
float SLOWPOLL    = 300.0 ;
float MEDPOLL     = 60.0 ;
float FASTPOLL    = 2.0 ;
float DLG_TIMEOUT = 30.0 ;

// misc
string  NOTECARD_NAME     = "secrets" ;
string  CLIENT_SECRET_KEY = "CLIENT_SECRET" ;
integer N_ROOMS_PARAMS    = 2 ;
integer N_MESSAGE_PARAMS  = 3 ;


// runtime vars
integer NotecardLineN  = 0 ;
list    Rooms          = [] ;
list    Messages       = [] ;
float   PollRate       = 0.0 ;
integer LastDlg        = 0 ;
integer LastLocalChat  = 0 ;
integer LastRemoteChat = 0 ;
integer LastMsgId      = 0 ;
integer ShouldResume   = TRUE ;
key     NotecardReqId ; // default::state_entry()
key     LoginReqId ;    // default::state_entry()
key     PingReqId ;     // default::http_response()
key     MessagesReqId ; // ready::timer()
key     RoomsReqId ;    // bridging::state_entry()
key     BridgeReqId ;   // bridging::listen()
key     AdminId ;       // ready::touch_start()
integer ChatListener ;  // ready::state_entry()
integer CtrlListener ;  // ready::touch_start()


/* state default functions */

Init()
{
  if (OWNER_NAME == "") SayState(INIT_MSG) ; else SayState(RESETTING_MSG) ;

  list parcel_details = llGetParcelDetails(llGetPos() , [PARCEL_DETAILS_NAME]) ;
  OWNER_NAME          = llKey2Name(llGetOwner()) ;
  REGION_NAME         = StrReplace(llGetRegionName()                 , " " , "_") ;
  PARCEL_NAME         = StrReplace(llList2String(parcel_details , 0) , " " , "_") ;
  CLIENT_NAME         = llDumpList2String([ GRID_NAME , REGION_NAME , PARCEL_NAME ] , " ") ;

  PING_PATH        = StrReplace(PING_PATH     , ID_SENTINAL , llEscapeURL(CLIENT_NAME)) ;
  ROOMS_PATH       = StrReplace(ROOMS_PATH    , ID_SENTINAL , llEscapeURL(CLIENT_NAME)) ;
  BRIDGE_PATH      = StrReplace(BRIDGE_PATH   , ID_SENTINAL , llEscapeURL(CLIENT_NAME)) ;
  MESSAGE_PATH     = StrReplace(MESSAGE_PATH  , ID_SENTINAL , llEscapeURL(CLIENT_NAME)) ;
  MESSAGES_PATH    = StrReplace(MESSAGES_PATH , ID_SENTINAL , llEscapeURL(CLIENT_NAME)) ;
  LOGIN_URL        = llToLower(SERVER_URL) + CLIENTS_PATH ;
  PING_URL         = llToLower(SERVER_URL) + PING_PATH ;
  ROOMS_URL        = llToLower(SERVER_URL) + ROOMS_PATH ;
  BRIDGE_URL       = llToLower(SERVER_URL) + BRIDGE_PATH ;
  MESSAGE_URL      = llToLower(SERVER_URL) + MESSAGE_PATH ;
  MESSAGES_URL     = llToLower(SERVER_URL) + MESSAGES_PATH ;
  JSON_POST_PARAMS = llListReplaceList(JSON_POST_PARAMS , SHOULD_VERIFY_SSL , -1 , -1) ;

  NotecardLineN  = 0 ;
  Rooms          = [] ;
  Messages       = [] ;
  PollRate       = 0.0 ;
  LastDlg        = 0 ;
  LastLocalChat  = 0 ;
  LastRemoteChat = 0 ;
  LastMsgId      = 0 ;
  NotecardReqId  = NULL_KEY ;
  LoginReqId     = NULL_KEY ;
  RoomsReqId     = NULL_KEY ;
  MessagesReqId  = NULL_KEY ;
  BridgeReqId    = NULL_KEY ;
  AdminId        = llGetOwner() ;
  ChatListener   = 0 ;
  CtrlListener   = 0 ;
}

key SendLoginRequest() { return SendRequest(LOGIN_URL , "" , "" , "") ; }

key SendPingRequest() { return SendRequest(PING_URL , "" , "" , "") ; }

integer ParseLoginResponse(string body)
{
  string client_secret ; string room_secret ;

  body          = ParseSubstring(body , SECRET_TOKEN      , SECRET_TOKEN_LEN     ) ;
  client_secret = llGetSubString(body , 0 , llSubStringIndex(body , STRING_SEP_TOKEN) - 1) ;
  body          = ParseSubstring(body , ROOM_SECRET_TOKEN , ROOM_SECRET_TOKEN_LEN) ;
  room_secret   = llGetSubString(body , 0 , llSubStringIndex(body , STRING_END_TOKEN) - 1) ;

  if (client_secret == "" || room_secret == "") { SayState(SERVER_ERROR_MSG) ; return FALSE ; }

DBG_DUPLICATE_SECRET(client_secret) ;

  if (client_secret == DUPLICATE_SECRET) client_secret = CLIENT_SECRET ;
  if (client_secret == ""              ) client_secret = NOTECARD_CLIENT_SECRET ;
  if (client_secret == ""              ) { SayState(INVALID_SECRET_MSG) ; return FALSE ; }

  CLIENT_SECRET = client_secret ;
  ROOM_SECRET   = room_secret   ;

  return TRUE ;
}

DisplaySecrets()
{
  if (NOTECARD_CLIENT_SECRET != CLIENT_SECRET)
    AdminSay("## copy these lines into the notecard: '" + NOTECARD_NAME + "' ## " +
             "\nCLIENT_SECRET = " + (CLIENT_SECRET)                               +
             "\nROOM_SECRET   = " + (ROOM_SECRET  )                               ) ;
}


/* state ready functions */

integer QueueMessage(string name , key id , string msg)
{
  if (llGetAgentSize(id) == ZERO_VECTOR) return FALSE ; // not an avatar

  if (HAS_CUSTOM_NAMES) name = StrReplace(name , " " , "_") ;
  else                  name = llList2String(llParseString2List(name , [" "] , []) , 0) ;

  Messages += [ name , msg , llGetUnixTime() ] ;

  return TRUE ;
}

SetPollRate(float poll_rate)
{
  if (PollRate != poll_rate) { PollRate = poll_rate ; llSetTimerEvent(PollRate) ; }
}

Stop()
{
  ShouldResume = !!ChatListener ;

  SayState(IDLE_MSG) ; llListenRemove(ChatListener) ; ChatListener = 0 ;
}

ThrottlePollRate(integer now)
{
  if ((now - LastDlg) > DLG_TIMEOUT) { llListenRemove(CtrlListener) ; CtrlListener = 0 ; }

  if      ((now - LastLocalChat ) > TIMEOUT) SetPollRate(SLOWPOLL) ;
  else if ((now - LastRemoteChat) > MEDPOLL) SetPollRate(MEDPOLL ) ;

DBG_STATS(now) ;
}

PostMessages()
{
  while (llGetListLength(Messages))
  {
    string nick    = llList2String(Messages , 0) ;
    string message = llList2String(Messages , 1) ;
    Messages       = llDeleteSubList(Messages , 0 , N_MESSAGE_PARAMS - 1) ;

    SendRequest(MESSAGE_URL , "" , nick , message) ;
  }
}

key SendMessagesRequest() { return SendRequest(MESSAGES_URL , "" , "" , "") ; }

list ParseMessagesResponse(string body)
{
  list messages = [] ; integer n_messages = 0 ; string id ; string nick ; string text ;

  do
  {
    body = ParseSubstring(body , ID_TOKEN   , ID_TOKEN_LEN  ) ;
    id   = llGetSubString(body , 0 , llSubStringIndex(body , INT_SEP_TOKEN   ) - 1) ;
    body = ParseSubstring(body , NICK_TOKEN , NICK_TOKEN_LEN) ;
    nick = llGetSubString(body , 0 , llSubStringIndex(body , STRING_SEP_TOKEN) - 1) ;
    body = ParseSubstring(body , TEXT_TOKEN , TEXT_TOKEN_LEN) ;
    text = llGetSubString(body , 0 , llSubStringIndex(body , STRING_SEP_TOKEN) - 1) ;

    if (id != "" && nick != "" && text != "") messages += [ id , nick , text ] ;

DBG_PARSE_MSGS(id , nick , text) ;
  }
  while (n_messages < (n_messages = (llGetListLength(messages) / N_MESSAGE_PARAMS))) ;

  return messages ;
}

DisplayMessages(list messages)
{
  integer message_n ; integer n_messages = llGetListLength(messages) / N_MESSAGE_PARAMS ;

  for (message_n = 0 ; message_n < n_messages ; ++message_n)
  {
    integer offfset = N_MESSAGE_PARAMS * message_n ;
    LastMsgId       = llList2Integer(messages , offfset + 0) ;
    string nick     = llList2String (messages , offfset + 1) ;
    string text     = llList2String (messages , offfset + 2) ;

    llSay(PUBLIC_CHANNEL , llUnescapeURL(nick + ": " + text)) ;
  }
}


/* state bridging functions */

key SendRoomsRequest() { return SendRequest(ROOMS_URL , "" , "" , "") ; }

ParseRoomsResponse(string body)
{
  Rooms = [] ; integer n_rooms = 0 ; string secret ; string clients_csv ;

  do
  {
    body        = ParseSubstring(body , SECRET_TOKEN  , SECRET_TOKEN_LEN ) ;
    secret      = llGetSubString(body , 0 , llSubStringIndex(body , STRING_SEP_TOKEN) - 1) ;
    body        = ParseSubstring(body , CLIENTS_TOKEN , CLIENTS_TOKEN_LEN) ;
    clients_csv = llGetSubString(body , 0 , llSubStringIndex(body , STRING_END_TOKEN) - 1) ;

    if (secret != "" && clients_csv != "") Rooms += [ secret , clients_csv ] ;

DBG_PARSE_ROOMS(secret , clients_csv) ;
  }
  while (n_rooms < (n_rooms = (llGetListLength(Rooms) / N_ROOMS_PARAMS))) ;
}

integer PromptRooms()
{
  integer room_n ; integer n_rooms = llGetListLength(Rooms) / N_ROOMS_PARAMS ;

  if (n_rooms == 0) return FALSE ;

  AdminSay(ROOMS_MSG) ;
  for (room_n = 0 ; room_n < n_rooms ; ++room_n)
  {
    string clients_csv = llUnescapeURL(llList2String(Rooms , (room_n * N_ROOMS_PARAMS) + 1)) ;
    string join        = "\n        " ;
    AdminSay("#" + (string)(room_n + 1) + ":" + join + StrReplace(clients_csv , "," , join)) ;
  }
  AdminSay(BRIDGE_PROMPT) ;

  return TRUE ;
}

key SendBridgeRequest(string room_secret) { return SendRequest(BRIDGE_URL , room_secret , "" , "") ; }

ParseBridgeResponse(string body)
{
  string room_secret ; string clients_csv ;

  body        = ParseSubstring(body , ROOM_SECRET_TOKEN , ROOM_SECRET_TOKEN_LEN) ;
  room_secret = llGetSubString(body , 0 , llSubStringIndex(body , STRING_END_TOKEN) - 1) ;
  body        = ParseSubstring(body , CLIENTS_TOKEN , CLIENTS_TOKEN_LEN) ;
  clients_csv = llGetSubString(body , 0 , llSubStringIndex(body , STRING_END_TOKEN) - 1) ;

  if (room_secret == "" || clients_csv == "") SayState(BRIDGE_FAIL_MSG) ;
  else                                        SayState(JOINED_ROOM_MSG + clients_csv) ;
}


/* helpers */

integer IsAdmin(key id) { return id == llGetOwner() ; }

AdminSay(string msg) { llInstantMessage(AdminId , msg) ; }

SayState(string msg) { llSay(PUBLIC_CHANNEL , msg) ; llSetText(msg , TEXT_COLOR , 1.0) ; }

key SendRequest(string url , string room_secret , string nick , string message)
{
  string client_key    = "'client': " ;
  string secret_json   = "{ 'secret': '" + CLIENT_SECRET + "' }" ;
  string client_params = secret_json ;
  string extra_params  = "" ;
  string request_json ;

  if      (url == LOGIN_URL   )
    client_params = "{ 'name':  '" + CLIENT_NAME + "' , " +
                    "  'nicks': '" + OWNER_NAME  + "' , " +
                    "  'type':  '" + "SimClient" + "' } " ;
  else if (url == ROOMS_URL   ) ;
  else if (url == BRIDGE_URL  )
    extra_params = " , 'room_secret': '" + room_secret + "'" ;
  else if (url == MESSAGE_URL )
    extra_params = " , 'message': { 'nick': '" + nick                 + "' , " +
                   "                'text': '" + llEscapeURL(message) + "' } " ;
  else if (url == MESSAGES_URL)
    extra_params = " , 'last_id': '" + (string)LastMsgId + "'" ;

  request_json = "{ " + client_key + client_params + extra_params + " }" ;
  request_json = StrReplace(request_json , "'" , "\"") ;

DBG_SEND_REQ(url , request_json) ;

  return llHTTPRequest(url , JSON_POST_PARAMS , request_json) ;
}

string StrReplace(string str , string sentinel , string replace)
{
  return llDumpList2String(llParseStringKeepNulls(str , [sentinel] , []) , replace) ;
}

string ParseSubstring(string search_string , string token , integer offset)
{
  integer idx ; string substring = "" ;

  if (~(idx = llSubStringIndex(search_string , token)))
    substring = llGetSubString(search_string , idx + offset , -1) ;

  return substring ;
}


/* debug */

DBG(string dbg) { if (DEBUG) llOwnerSay(dbg) ; }

DBG_STATS(integer now)
{
  if (!DEBUG) return ;

  string polling_msg = "polling @ "       + (string)((integer)PollRate   ) + " sec" ;
  string local_msg   = "LastLocalChat: "  + (string)(now - LastLocalChat ) + " sec" ;
  string remote_msg  = "LastRemoteChat: " + (string)(now - LastRemoteChat) + " sec" ;

  DBG("DBG_STATS() " + polling_msg + " - " + local_msg + " " + remote_msg) ;
  if      (DBG_T = (++DBG_T % 5) < 3) llSetText(polling_msg + "\n" + local_msg + "\n" + remote_msg , TEXT_COLOR , 1.0) ;
  else if (ChatListener != 0        ) llSetText(RUNNING_MSG , TEXT_COLOR , 1.0) ;
  else if (ChatListener == 0        ) llSetText(IDLE_MSG    , TEXT_COLOR , 1.0) ;
} integer DBG_T = 0 ;

DBG_DUPLICATE_SECRET(string client_secret)
{
  if (client_secret != DUPLICATE_SECRET) return ;

  string dbg = "ParseLoginResponse() DUPLICATE_SECRET - trying " ;
  if (CLIENT_SECRET != "") DBG(dbg + "CLIENT_SECRET="          + CLIENT_SECRET         ) ;
  else                     DBG(dbg + "NOTECARD_CLIENT_SECRET=" + NOTECARD_CLIENT_SECRET) ;
}

DBG_SEND_REQ(string url , string request_json)
{
  DBG("SendRequest() url=" + url + "\nrequest_json=" + request_json) ;
}

DBG_PARSE_ROOMS(string secret , string clients_csv)
{
  DBG("ParseRoomsResponse() secret='" + secret + "' clients_csv='" + clients_csv + "'") ;
}

DBG_PARSE_MSGS(string id , string nick , string text)
{
  DBG("ParseMessagesResponse() id='" + id + "' nick='" + nick + "' text='" + text + "'") ;
}


default
{
  state_entry()
  {
    Init() ;
    if (llGetInventoryKey(NOTECARD_NAME) != NULL_KEY)
      NotecardReqId = llGetNotecardLine(NOTECARD_NAME , NotecardLineN) ;
    else LoginReqId = SendLoginRequest() ;
  }

  dataserver(key resp_id , string data)
  {
    if (resp_id != NotecardReqId) return ;

    if (data != EOF)
    {
      list kvp = llParseString2List(data , ["="] , []) ;
      string k = llStringTrim(llList2String(kvp , 0) , STRING_TRIM) ;
      string v = llStringTrim(llList2String(kvp , 1) , STRING_TRIM) ;

      if (k == CLIENT_SECRET_KEY) NOTECARD_CLIENT_SECRET = v ;

      NotecardReqId = llGetNotecardLine(NOTECARD_NAME , ++NotecardLineN) ;
    }
    else LoginReqId = SendLoginRequest() ;
  }

  http_response(key resp_id , integer status , list data , string body)
  {
    if (resp_id == LoginReqId && status == HTTP_STATUS_SUCCESS)
    {
      LoginReqId = NULL_KEY ;

      if (ParseLoginResponse(body)) PingReqId = SendPingRequest() ;
    }
    else if (resp_id == PingReqId)
    {
      PingReqId = NULL_KEY ;
      if      (status == HTTP_STATUS_SUCCESS     ) state ready ;
      else if (status == HTTP_STATUS_UNAUTHORIZED) SayState(INVALID_SECRET_MSG) ;
    }
  }
}


state ready
{
  state_entry()
  {
    if (ShouldResume) SayState(RUNNING_MSG) ; else { SayState(READY_MSG) ; return ; }

    ChatListener = llListen(PUBLIC_CHANNEL , "" , NULL_KEY , "" ) ;

    LastLocalChat = LastRemoteChat = llGetUnixTime() - (integer)MEDPOLL ;
    SetPollRate(FASTPOLL) ;
  }

  touch_start(integer n)
  {
    string dialog_prompt ;
    if (ChatListener == 0) dialog_prompt = DLG_IDLE_PROMPT ;
    else                   dialog_prompt = DLG_RUNNING_PROMPT ;

    key  id       = llDetectedKey(0) ;
    list dlg_btns = [ OFF_DLG_BTN , ON_DLG_BTN ] ;

    if (IsAdmin(id)) { dlg_btns += BRIDGE_DLG_BTN ; AdminId = id ; }
    llDialog(id , dialog_prompt , dlg_btns , DLG_CH) ;

    CtrlListener = llListen(DLG_CH , "" , id , "") ;
    LastDlg      = llGetUnixTime() ;
  }

  listen(integer ch , string name , key id , string msg)
  {
    if      (ch == PUBLIC_CHANNEL) { if (QueueMessage(name , id , msg)) SetPollRate(FASTPOLL) ; }
    else if (ch == DLG_CH        )
      if      (msg == BRIDGE_DLG_BTN && IsAdmin(id)) { Stop() ; state bridging ; }
      else if (msg == OFF_DLG_BTN                  ) { Stop() ;                  }
      else if (msg == ON_DLG_BTN                   ) { state default ;           }
  }

  timer()
  {
    integer now = llGetUnixTime() ;

    if (llGetListLength(Messages) == 0) { ThrottlePollRate(now) ;                }
    else                                { PostMessages() ; LastLocalChat = now ; }

    MessagesReqId = SendMessagesRequest() ;
  }

  http_response(key resp_id , integer status , list data , string body)
  {
    if (resp_id == MessagesReqId     ) MessagesReqId = NULL_KEY ;
    if (status != HTTP_STATUS_SUCCESS) return ;

    DisplayMessages(ParseMessagesResponse(body)) ; LastRemoteChat = llGetUnixTime() ;
  }
}


state bridging
{
  state_entry() { llSetTimerEvent(DLG_TIMEOUT) ; RoomsReqId = SendRoomsRequest() ; }

  http_response(key resp_id , integer status , list data , string body)
  {
    if (status != HTTP_STATUS_SUCCESS) return ;

    if      (resp_id == RoomsReqId )
    {
      RoomsReqId  = NULL_KEY ; ParseRoomsResponse(body) ;

      if (PromptRooms()) ChatListener = llListen(PUBLIC_CHANNEL , "" , AdminId , "") ;
      else llSetTimerEvent(1.0) ; // bail
    }
    else if (resp_id == BridgeReqId)
    {
      BridgeReqId = NULL_KEY ; ParseBridgeResponse(body) ; llSetTimerEvent(1.0) ; // done
    }
  }

  listen(integer ch , string name , key id , string msg)
  {
    if (!!llSubStringIndex(msg , "#")) return ;

    integer room_n      = (integer)llGetSubString(msg , 1 , -1) - 1 ;
    integer n_rooms     = llGetListLength(Rooms) / N_ROOMS_PARAMS ;
    string  room_secret = llList2String(Rooms , (room_n * N_ROOMS_PARAMS) + 0) ;

    if (room_n < 0 || room_n >= n_rooms) return ;

    llListenRemove(ChatListener) ; ChatListener = 0 ;
    BridgeReqId = SendBridgeRequest(room_secret) ;
  }

  timer()
  {
    if      (RoomsReqId != NULL_KEY || BridgeReqId != NULL_KEY) SayState(BRIDGE_TIMEOUT_MSG   ) ;
    else if (llGetListLength(Rooms) == 0                      ) SayState(NO_ROOMS_MSG         ) ;
    else if (ChatListener != 0                                ) SayState(BRIDGE_SELECT_TIMEOUT) ;

    llListenRemove(ChatListener) ; ChatListener = 0 ;

    if (ShouldResume) state default ;
    else              state ready ;
  }
}
